查看原文
其他

java泛型特性,你了解多少?

炸鸡可乐 Java极客技术 2021-02-01

大家好,我是本周的值班编辑 江南一点雨 ,本周将由我为大家排版并送出技术干货,大家可以在公众号后台回复“java”,获得作者 Java 知识体系/面试必看资料。



对java泛型特性的了解,很多时候是从集合对象接触到的,今天小编带大家一起去深入的了解泛型的缘由和使用方式!

01、泛型的由来

小编想告诉大家的是:泛型的产生本质是来源于软件设计!

在软件设计的过程中经常会用到容器类,容器类代码都一样只是数据类型不同,如果能够让一种类型容纳所有类型,就可以实现代码重用,但是没有一种类型可以容纳所有类型,为了解决容器的问题,由此就产生了泛型设计。

由此可见,泛型是一个不确定的参数类型,即“参数化类型”!

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

那么怎么使用泛型,进行软件设计呢?

02、使用方式

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

2.1、泛型类

泛型类型用于类的定义中,典型的就是各种容器类,如:List、Set、Map。

最普通的自定义泛型类

  1. /**

  2. * 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型

  3. * 在实例化泛型类时,必须指定T的具体类型

  4. */

  5. public class Generic<T>{

  6. /**key这个成员变量的类型为T,T的类型由外部指定*/

  7. private T key;


  8. /**泛型构造方法形参key的类型也为T,T的类型由外部指定*/

  9. public Generic(T key) {

  10. this.key = key;

  11. }


  12. /**泛型方法getKey的返回值类型为T,T的类型由外部指定*/

  13. public T getKey(){

  14. return key;

  15. }

  16. }

传参方式

  1. //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型

  2. //传入的实参类型需与泛型的类型参数类型相同,即为Integer.

  3. Generic<Integer> genericInteger = new Generic<Integer>(12345678);


  4. //传入的实参类型需与泛型的类型参数类型相同,即为String.

  5. Generic<String> genericString = new Generic<String>("hello");

  6. System.out.println("泛型测试","key is " + genericInteger.getKey());

  7. System.out.println("泛型测试","key is " + genericString.getKey());

输出结果

  1. 泛型测试: key is 12345678

  2. 泛型测试: key is hello

定义的泛型类,就一定要传入泛型类型实参么?

并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

举个例子

  1. Generic generic = new Generic("111111");

  2. Generic generic1 = new Generic(4444);

  3. Generic generic2 = new Generic(55.55);

  4. Generic generic3 = new Generic(false);


  5. System.out.println("泛型测试","key is " + generic.getKey());

  6. System.out.println("泛型测试","key is " + generic1.getKey());

  7. System.out.println("泛型测试","key is " + generic2.getKey());

  8. System.out.println("泛型测试","key is " + generic3.getKey());

输出结果

  1. 泛型测试: key is 111111

  2. 泛型测试: key is 4444

  3. 泛型测试: key is 55.55

  4. 泛型测试: key is false

总结:

1、泛型的类型参数只能是类类型,不能是简单类型。2、不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。如下:

  1. if(ex_num instanceof Generic<Number>){

  2. }

2.2、泛型接口

泛型接口与泛型类的定义及使用基本相同,泛型接口常被用在各种类的生产器中。

  1. /**

  2. * 定义一个泛型接口

  3. */

  4. public interface Generator<T> {

  5. public T next();

  6. }

当实现泛型接口的类,未传入泛型实参时

  1. /**

  2. * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中

  3. *

  4. * 如果不声明泛型,编译器会报错:"Unknown class"

  5. */

  6. public class FruitGenerator<T> implements Generator<T>{

  7. @Override

  8. public T next() {

  9. return null;

  10. }

  11. }

当实现泛型接口的类,传入泛型实参时

  1. /**

  2. * 传入泛型实参时:

  3. * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>

  4. * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。

  5. * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型

  6. * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。

  7. */

  8. public class FruitGenerator implements Generator<String> {


  9. @Override

  10. public String next() {

  11. return 'hello world';

  12. }

  13. }

2.3、泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 。

  1. /**

  2. * 泛型方法的基本介绍

  3. * @param tClass 传入的泛型实参

  4. * @return T 返回值为T类型

  5. */

  6. public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,

  7. IllegalAccessException{

  8. T instance = tClass.newInstance();

  9. return instance;

  10. }

说明:

1、public 与 返回值中间 <T>非常重要,可以理解为声明此方法为泛型方法。2、只有声明了 <T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法,如get、set。3、 <T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如 TEKV等形式的参数常用于表示泛型。

方法调用方式

  1. //通过泛型方法,实例化一个Test对象

  2. Object obj = genericMethod(Class.forName("com.test.Test"));

03、其他使用介绍

3.1、泛型通配符

还是举例子,我们知道 IngeterNumber的一个子类,那么问题来了,在使用 Generic<Number>作为形参的方法中,能否使用 Generic<Ingeter>的实例传入呢?

为了弄清楚这个问题,我们使用 Generic<T>这个泛型类做例子:

  1. public class Generic<T> {


  2. private T key;


  3. public T getKey() {

  4. return key;

  5. }


  6. public void setKey(T key) {

  7. this.key = key;

  8. }


  9. public Generic() {

  10. super();

  11. // TODO Auto-generated constructor stub

  12. }


  13. public Generic(T key) {

  14. super();

  15. this.key = key;

  16. }

  17. }

测试

  1. public class GenericTest {


  2. public static void main(String[] args) {

  3. //编译器会为我们报错:Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>

  4. Generic<Integer> generic1 = new Generic<Integer>(1);

  5. new GenericTest().showKeyValue(generic1);

  6. }


  7. public void showKeyValue(Generic<Number> obj){

  8. System.out.println("泛型测试:key value is " + obj.getKey());

  9. }

  10. }

通过提示信息我们可以看到 Generic<Integer>不能被看作为 Generic<Number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

如何解决上面的问题?总不能为了定义一个新的方法来处理 Generic<Integer>类型的类,这显然与java中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是 Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。

将上面的方法改一下:

  1. public void showKeyValue(Generic<?> obj){

  2. System.out.println("泛型测试:key value is " + obj.getKey());

  3. }

类型通配符一般是使用 ?代替具体的类型实参。

3.2、泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

  • 上界通配符

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型!例如:

  1. public void showKeyValue(Generic<? extends Number> obj) {

  2. System.out.println("泛型测试:key value is " + obj.getKey());

  3. }

测试

  1. public static void main(String[] args) {

  2. Generic<Integer> generic1 = new Generic<Integer>(1);

  3. //编译器会提示错误,因为String类型并不是Number类型的子类

  4. Generic<String> generic2 = new Generic<String>("1");

  5. new GenericTest().showKeyValue(generic1);

  6. new GenericTest().showKeyValue(generic2);

  7. }

  • 下界通配符

下界通配符的意思是容器中只能存放T及其T的基类类型的数据,说白了,就是跟上界相反的过程

  1. public void showKeyValue(Generic<? super Integer> obj) {

  2. System.out.println("泛型测试:key value is " + obj.getKey());

  3. }

测试

  1. public static void main(String[] args) {

  2. Generic<Integer> generic1 = new Generic<Integer>(1);

  3. Generic<Number> generic2 = new Generic<Number>(100);

  4. new GenericTest().showKeyValue(generic1);

  5. new GenericTest().showKeyValue(generic2);

  6. }

输出结果

  1. 泛型测试:key value is 1

  2. 泛型测试:key value is 100

最后简单介绍下Effective Java这本书里面介绍的PECS原则。

  • 上界不能往里存,只能往外取,适合频繁往外面读取内容的场景。

  • 下界不影响往里存,但往外取只能放在Object对象里,适合经常往里面插入数据的场景。

是不是听的很懵,没关系,看上面的例子就可以了,哈哈哈!

04、总结

本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。


精彩回顾:

探究Java中的final关键字

Spring 源码学习(四) bean 的加载

手把手教你用 Docker 部署网站[Docker 系列-8]

强烈推荐:

《Java 极客技术》知识星球限时优惠,现在加入只需 50 元,仅限前 1000 名,机不可失时不再来。长按识别下面的二维码即可加入,趁早行动吧!



隆重介绍:

Java 极客技术公众号,是由一群热爱 Java 开发的技术人组建成立,专注分享原创、高质量的 Java 文章。如果您觉得我们的文章还不错,请帮忙赞赏、在看、转发支持,鼓励我们分享出更好的文章。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存